/*
    数据库管理模块说明文档

实现一个我们自己的mysql客户端来访问服务器进行数据的操作
针对我们管理的每一张表都设计一个类,通过类实例化的对象管理指定的数据库表
class user_table{
private:
    MYSQL* _mysql;            // mysql操作句柄
    std::mutex _mutex;        // 互斥锁保护数据库的访问操作
public:
    user_table(){}
    ~user_table(){}
    bool insert(Json::Value &user);                                   // 注册新用户(同时需要user的username和password)
    bool login(Json::Value &user);                                    // 登录验证,并返回详细的用户信息(通过 Json::Value &user 返回出来)
    bool select_by_name(const std::string &name, Json::Value &user);  // 通过用户username获取用户信息(通过 Json::Value &user 返回出来)
    bool select_by_id(uint16_t &id, Json::Value &user);               // 通过用户id获取用户的信息(通过 Json::Value &user 返回出来)
    bool win(uint16_t &id);                                           // 胜利时天梯分数增加,战斗场次增加,胜利场次增加
    bool lose(uint16_t &id);                                          // 胜利时天梯分数减少,战斗场次增加,其它的不变的哦
};

*/

#ifndef __M_DB_H__
#define __M_DB_H__

#include "util.hpp"

#include <mutex>
#include <cassert>

class user_table{
private:
    MYSQL* _mysql;        // mysql 操作句柄
    std::mutex _mutex;   // 互斥锁保护数据的访问操作
    // 因为 mysql_exec 和 mysql_store_result 函数各自单独是线程安全(可重入函数)的，但是两个放在一起的时候就得加锁了

public:
    user_table(const std::string &host,
        const std::string &username,
        const std::string &password,
        const std::string &dbname,
        uint16_t port = 3306){
        _mysql = mysql_util::mysql_create(host, username, password, dbname, port);
        assert(_mysql != nullptr);
    }  

    ~user_table(){
        mysql_util::mysql_destroy(_mysql);
        _mysql = nullptr;
    }

    // 注册新用户
    bool insert(Json::Value &user){
#define INSERT_USER "insert user value(null, '%s', password('%s'), 1000, 0, 0)"
        
        // 用户不存在的情况下，才会有可能注册的哦
        // 因为我们的数据库是使用username作为唯一主键的，所以下面的代码可以注释了
        // Json::Value val;
        // bool ret = select_by_name(user["username"].asCString(), val);
        // if(ret == true){
        //     DLOG("user:%s is already exists", user["username"]);
        //     return false;
        // }

        // sprintf(void* buf, *format, ...);
        // 格式化的数据写入 sql 里面

        // 注册新用户的话，是同时得有 username 和 password 的哦。所以得加下面的判断
        // == nullptr 是不行的，得用他们自己的库的哦
        // if(user["username"].asCString() == "" || user["password"],asCString() == ""){ 
        // 下面的判断的写法是根据 #include <mysql/mysql.h> 里面的自定义的函数来写的 --> 得自己去查安装的 mysql.h 头文件里面去找的哦
        // 在用户主目录下配置一个 vim 的配置文件(一般新创建的用户是没有vim的配置文件的,因为当前服务器的多个用户里面,每一个人所需要的vim的配置文件不同.所以默认没有吧 --> 可百度)
        // 就配置 vim 现实一下行号的问题 --> touch .vimrc(以 . 开头的说明是一个隐藏文件) --> vim .vimrc 进入 .vimrc 文件 --> 在最后一行加入 set nu 再退出 --> 再用 vim 打开其它的文件的时候就有行号了.
        // 再 vim /usr/include/jsoncpp/json/value.h(我们用的是绝对路径打开的,所以可以在任意路径下打开,查看 json 库自己定义的函数) 
        // --> 343 行有声明 bool isNull() const; 函数 --> 应该是我们想要的
        // --> 203 行有声明 static const UInt64 maxUInt64 --> 下面用的 stol 函数需要强转成 long long 类型的需要用的
        if(user["username"].isNull() || user["password"].isNull()){
        // 下面的写法是更加容易理解的，但还是用库里面的标准的写法吧
        // if(select_by_id(user["id"].asInt(), user) == false || select_by_name(user["username"].asCString(), user) == false){
            DLOG("plase input username and password!\n");
            return false;
        }
        char sql[4096] = {0};  
        sprintf(sql, INSERT_USER, user["username"].asCString(), user["password"].asCString());
        bool ret = mysql_util::mysql_exec(_mysql, sql);
        if(ret == false){
            ELOG("insert user failed!\n");
            return false;   
        }
        return true;

        // 问题：
        // 如果这个函数是上面的判断的话，是有问题的：
        // 当外面我们调用 insert 函数的时候，我们如果初始化了username和password其中的一个就会有下面的报错信息的哦。
        // terminate called after throwing an instance of 'Json::LogicError'
        // what():  in Json::Value::asCString(): requires stringValue
        // Aborted   

        // gdb 定位问题：
        // (使用 gdb 的时候可能得在 makefile 文件里面加上 -g 选项才会有报错信息的行数的哦)
        // make 编译一下先，再 gdb ./gobang,就运行 gdb 调试了。
        // 输入 run 就跑来了，再输入 bt 查看函数调用栈信息
        // (gdb) bt
        // #0  0x00007ffff6767387 in raise () from /lib64/libc.so.6
        // #1  0x00007ffff6768a78 in abort () from /lib64/libc.so.6
        // #2  0x00007ffff7077a95 in __gnu_cxx::__verbose_terminate_handler() () from /lib64/libstdc++.so.6
        // #3  0x00007ffff7075a06 in ?? () from /lib64/libstdc++.so.6
        // #4  0x00007ffff7075a33 in std::terminate() () from /lib64/libstdc++.so.6
        // #5  0x00007ffff7075c53 in __cxa_throw () from /lib64/libstdc++.so.6
        // #6  0x00007ffff733c5c7 in Json::throwLogicError(std::string const&) () from /lib64/libjsoncpp.so.0
        // #7  0x00007ffff733d429 in Json::Value::asCString() const () from /lib64/libjsoncpp.so.0
        // #8  0x0000000000403d33 in user_table::insert (this=0x7fffffffd7f0, user=...) at db.hpp:56
        // #9  0x0000000000402dca in db_test () at gobang.cc:73
        // #10 0x0000000000402ec4 in main () at gobang.cc:99
        // (gdb) 
        // 看上面的 gdb 调试的信息可以看出来：后面有行号的一般是出现问题的地方的哦。
        //  --> "db.hpp" 里面的 user_table::insert 函数在 56 行室友问题的。
        // 就是有一个信息获取不到了
    }

    // 登录验证
    bool login(Json::Value &user){
        // 以用户名和用户id共同作为查询过滤条件，查询到数据则表示用户名和密码一致，没有信息则用户名和密码错误
// #define LOGIN_USER "select id, score, total_count, win_count from user where username='%s' and password=password('%s');"
#define LOGIN_USER "select id, score, total_count, win_count from user where username='%s' and password=password('%s');"
        char sql[4096] = {0};
        sprintf(sql, LOGIN_USER, user["username"].asCString(), user["password"].asCString());
        
        // {
        //     _mutex.lock();
        //     bool ret = mysql_util::mysql_exec(_mysql, sql);
        //     if(ret == false){
        //         ELOG("user login failed!\n");
        //         _mutex.unlock();
        //         return false;   
        //     }

        //     // 按理说，要么有数据，要么没有数据。就算有数据也只能有一条数据 
        //     MYSQL_RES *res = mysql_store_result(_mysql);
        //     if(res == nullptr){
        //         DLOG("have no login user info!");
        //         _mutex.unlock();
        //         return false;
        //     }
        //     _mutex.unlock();
        // }

        MYSQL_RES * res = nullptr;
        {
            // RAII 比上面的写法更好
            std::unique_lock<std::mutex> lock(_mutex);

            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if(ret == false){
                ELOG("user login failed!\n");
                return false;   
            }

            // 按理说，要么有数据，要么没有数据。就算有数据也只能有一条数据 
            res = mysql_store_result(_mysql);
            if(res == nullptr){
                DLOG("have no login user info!");
                return false;
            }
        }

        int row_num = mysql_num_rows(res);
        if(row_num != 1){
            DLOG("login user info not unique");
            return false;
        }
        MYSQL_ROW row = mysql_fetch_row(res);
        // uint64_t 是不行的哦，得用它自己封装的
        // user["id"] = (uint64_t)std::stol(row[0]);
        user["id"] = (Json::UInt64)std::stol(row[0]);
        user["score"] = (Json::UInt64)std::stol(row[1]);
        user["total_count"] = std::stoi(row[2]);
        user["win_count"] = std::stoi(row[3]);
        mysql_free_result(res);

        return true;
    }
    
    // 通过用户(name)获取用户信息
    bool select_by_name(const std::string &name, Json::Value &user){
        // 查询语句其实也是
#define USER_BY_NAME "select id, score, total_count, win_count from user where username='%s';"
        char sql[4096] = {0};
        sprintf(sql, USER_BY_NAME, name.c_str());
        
        MYSQL_RES * res = nullptr;
        {
            // RAII 比上面的写法更好
            std::unique_lock<std::mutex> lock(_mutex);

            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if(ret == false){
                DLOG("get user by name failed!\n");
                return false;   
            }

            // 按理说，要么有数据，要么没有数据。就算有数据也只能有一条数据 
            res = mysql_store_result(_mysql);
            if(res == nullptr){
                DLOG("have no user info!");
                return false;
            }
        }

        int row_num = mysql_num_rows(res);
        if(row_num != 1){
            DLOG("the user information queried is not unique!");
            return false;
        }
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = (Json::UInt64)std::stol(row[0]);
        user["username"] = name;
        user["score"] = (Json::UInt64)std::stol(row[1]);
        user["total_count"] = std::stoi(row[2]);
        user["win_count"] = std::stoi(row[3]);
        mysql_free_result(res);

        return true;
    }
    
    // 通过用户(id)获取用户信息
    bool select_by_id(uint16_t id, Json::Value &user){
// 查询语句其实也是
#define USER_BY_ID "select username, score, total_count, win_count from user where id='%d';"
        char sql[4096] = {0};
        sprintf(sql, USER_BY_ID, id);
        
        MYSQL_RES * res = nullptr;
        {
            // RAII 比上面的写法更好
            std::unique_lock<std::mutex> lock(_mutex);

            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if(ret == false){
                ELOG("get user by id failed!\n");
                return false;   
            }

            // 按理说，要么有数据，要么没有数据。就算有数据也只能有一条数据 
            res = mysql_store_result(_mysql);
            if(res == nullptr){
                DLOG("have no user info!");
                return false;
            }
        }

        int row_num = mysql_num_rows(res);
        if(row_num != 1){
            DLOG("the user information queried is not unique!");
            return false;
        }
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = (Json::UInt64)id;
        user["username"] = row[0];
        user["score"] = (Json::UInt64)std::stol(row[1]);
        user["total_count"] = std::stoi(row[2]);
        user["win_count"] = std::stoi(row[3]);
        mysql_free_result(res);

        return true;
    }
    
    // 胜利时天梯分数增加，战斗场次增加，胜利场次增加
    bool win(uint16_t id){
#define USER_WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id='%d';"
        char sql[4096] = {0};
        sprintf(sql, USER_WIN, id);
        bool ret = mysql_util::mysql_exec(_mysql, sql);
        if(ret == false){
            DLOG("update win user info failed!\n");
            return false;
        }

        return true;
    }
    
    // 胜利时天梯分数减少，战斗场次增加，其它不变
    bool lose(uint16_t id){
#define USER_LOSE "update user set score=score-30, total_count=total_count+1 where id='%d';"
        char sql[4096] = {0};
        sprintf(sql, USER_LOSE, id);
        bool ret = mysql_util::mysql_exec(_mysql, sql);
        if(ret == false){
            DLOG("update lose user info failed!\n");
            return false;
        }

        return true;
    }
};


#endif

